一、顶点属性和顶点缓冲区
概述:
想要绘制一个对象,它的顶点数据(pos,norm,color,uv…)需要被发送给顶点着色器。
通常会把顶点数据在C++端放入一个顶点缓冲区(VBO),并把顶点缓冲区和着色器中声明的顶点属性相关联。
布局:
布局1:数组的结构(SOA)
布局2:结构的数组(AOS)
VBO注释:
VBO:顶点缓存对象。(包含顶点缓冲区)
顶点缓冲对象VBO是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标,顶点法向量,顶点颜色数据等。在渲染时,可以直接从VBO中取出顶点的各类属性数据,由于VBO在显存而不是在内存中,不需要从CPU传输数据,处理效率更高。
https://blog.csdn.net/dcrmg/article/details/53556664
VAO注释:
顶点数组对象。(存储状态配置,是一种组织顶点缓冲区的方法,使之在复杂场景中更容易操控)(VAO不存储实际的数据。)
- VAO记录的是:
1.vertex attribute 的格式,由 glVertexAttribPointer 设置。
2.vertex attribute 对应的 VBO 的名字, 由一对 glBindBuffer 和 glVertexAttribPointer 设置。
3.当前#绑定的 GL_ELEMENT_ARRAY_BUFFER 的名字,由 glBindBuffer 设置。 - 补充:
顶点数组对象(VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。
这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。
这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。
Creedon:也许使用相同的着色器的模型可以用同一个VAO。
- 参考:
https://blog.csdn.net/qq_44800780/article/details/102998897
实现步骤:
初始化Init中:
- 〇创建顶点数组对象:
glGenVertexArray(1, vao) //创建至少一个顶点数组对象存进定义的GLuint数组vao。 glBindVertexArray(vao[0]); //把顶点数组对象标记为活跃。
- ①创建顶点缓冲区:
glGenBuffer(2, vbo); //创建两个VBO存进定义的GLuint数组vbo。
- ②将顶点数据复制到缓冲区:
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(vPosition), vPosition, GL_STATIC_DRAW); //将第0个缓冲区标记为活跃并把包含数据的数组复制进活跃缓冲区。
渲染display中:
- 〇活跃缓冲区:
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); //将第0个缓冲区设为活跃。
- ①缓冲区关联顶点属性:
glVertexAttibPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); //将第0个顶点属性关联到活跃缓冲区。
- ②启用顶点属性:
glEnableVertexAttribArray(0); //启用第0个顶点属性。
二、一致变量(Uniform Var)
概述:
一致变量即外部程序传给OpenGL的变量。例如要渲染一个3D场景,需要构建MV、P变换矩阵,习惯上会将这些矩阵从C++程序传给着色器的一致变量(包括顶点着色器和片段着色器)。
声明:
一致变量用uniform关键字声明。例如uniform mat4 mv_matrix;
。
传递的实现:
//获取着色器中一致变量的位置
mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
//将数据传到一致变量中
glUniformMaxtrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
三、顶点属性插值
光栅化过程会线性插值顶点属性值。
注意:一致变量本身不是插值的,它始终保持不变。
常被插值的属性有法线、纹理坐标uv、颜色等。
顶点输入/输出属性声明:
layout (location = 0) in vec3 position;
out color;
-
在顶点着色器中用
in
关键字声明顶点属性,表示它们从缓冲区接收值。还可以用out
声明顶点属性,表示它们会将值发送到管线下一个阶段。 -
从顶点着色器传到片段着色器中的变量约定用”
varying+变量名
“的命名方式。
内置的属性:
- 没必要为顶点位置声明一个
out
变量。因为有一个内置的gl_Position
表示变换后的顶点位置。
四、同一个模型的重复渲染
方式1: 重复调用glDrawArrays()
:在display()中构建差异。
方式2: 调用glDrawArraysInstanced()
:在着色器代码中构建差异。(OpenGL实例化机制)(差异因子由程序传入uniform变量)
五、多个模型的渲染
用多个顶点缓冲区(vbo)保存多个物体的顶点信息。
注意:
P矩阵可共用,MV矩阵要分别计算。
六、多层级模型渲染和矩阵堆栈
实际建模中通常会通过组装较小的简单模型构建复杂模型。这种方式构建的对象称为分层级模型。分层模型不仅可用于构建复杂对象,还可以用来生成复杂场景。
使用矩阵堆栈使得创建和管理复杂分层对象和场景变得容易。它使得变换可以构建在其他变换之上(或者从其他变换中被移除)。
※可以使用C++标准模板库(STL)的stack类。
堆栈结构和实现:
第一个入栈的是V矩阵。然后依次是父物体、子物体的MV矩阵。
如果在父对象上创建子对象时,使用push命令在堆栈顶部创建新的矩阵,然后再根据需要将期望的变换应用于堆栈顶部新创建的矩阵。(stack.push(stack.top())
)
应用新对象所需变换,也就是所需的变换乘以它。
补充:另一种VBO布局(AOS)
#define VERTEX_POS_INDX 0
#define VERTEX_POS_SIZE 3
#define VERTEX_COLOR_INDX 1
#define VERTEX_COLOR_SIZE 4
#define VERTEX_ATTRIB_SIZE (VERTEX_POS_SIZE+VERTEX_COLOR_SIZE)
GLfloat vertex[]={
0.0f,1.0f,1.0f, //x,y,z
1.0f,0.0f,0.0f,1.0f, //RGBA
1.0f,0.5f,1.0f,
0.0f,1.0f,0.0f,1.0f,
0.0f,0.5f,1.0f,
0.0f,0.0f,1.0f,1.0f
}
//设置顶点位置属性数据数组
glVertexAttribPointer(VERTEX_POS_INDX,VERTEX_POS_SIZE,GL_FLOAT,GL_FALSE,VERTEX_ATTRIB_SIZE*sizeof(GLfloat),vertex);
//设置颜色属性数据数组
glVertexAttribPointer(VERTEX_COLOR_INDX,VERTEX_COLOR_SIZE,GL_FLOAT,GL_FALSE,(VERTEX_ATTRIB_SIZE*sizeof(GLfloat),vertex+VERTEX_POS_SIZE));
//启用顶点数组数据
glEableVertexAttribArray(VERTEX_POS_INDE);
glEnableVertexAttribArray(VERTEX_COLOR_INDE);
//根据顶点数据绘制三角形
glDrawArrays(GL_TRIANGLES,0,3);
//禁用顶点数组数据
glDisableVertexAttribArray(VERTEX_POS_INDE);
glDisableVertexAttribArray(VERTEX_COLOR_INDE);
关于glVertexAttribPointer()
的参数:
-
参数1:指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = ?)
-
参数2:指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
-
参数3:指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
-
参数4:定义我们是否希望数据被标准化(Normalize)。
-
参数5:步长(Stride)。它告诉我们在连续的顶点属性组之间的间隔。
-
参数6:数据指针(类型是void*)。指针表示数据在缓冲中起始位置的偏移量。
补充:使用索引绘制(glDrawElements()
)
使用glDrawElements()
与glDrawArrays()
相比:
-
减少绘制呼叫数。每个绘制调用都会产生一些开销,用于验证状态、设置内容等,并且大部分开销发生在 CPU 端。通过减少绘制调用的数量,我们避免了大部分开销。
-
顶点重用。这不仅仅是节省内存。您的 GPU 可能具有硬件顶点缓存,可以存储最近转换的顶点;如果同一顶点再次出现,并且它出现在缓存中,则可以从缓存中重用它,而不必再次转换。您的硬件将通过比较索引来检查缓存,因此在 OpenGL 术语中,使用缓存的唯一方法就是使用 glDrawElements。
参数相关:
- 最后一个参数是(void*)类型的字节偏移量。
注意事项:
-
如果是简单的光滑模型,用索引方式就很简单。
-
当一个顶点允许多个nv坐标和法线(例如接缝)时,顶点数据存放会很麻烦,同样需要多次存储顶点。因为OpenGL不允许多属性分开索引。
setVertexBuffers():
//使用`glDrawElements()`则需要额外定义一个数组缓冲保存索引。
vector<int> ind = model.getIndicates();
//...
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, ind.size() * 4, &ind[0], GL_STATIC_DRAW);
display():
//...//把索引缓冲区激活后开始draw elements.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]);
glDrawElements(GL_TRIANGLES, model.getNumIndicates(), GL_UNISIGNED_INT, 0);
补充:使用glBegin()/glEnd()
绘制
用于固定管线。已被弃用。
补充:深度缓存和深度测试
OpenGL使用深度测试来进行隐藏面消除。使用glEnable(GL_DEPTH_TEST)
和glDepthFunc(GL_LEQUAL/GL_LESS)
开启深度测试并指定判断函数(小于等于或者小于时通过)。
在渲染帧之前要用glClear(GL_DEPTH_BUFFER_BIT)
清除深度缓冲区,它使用默认值1.0来填充。默认情况下,OpenGL深度值为0.0~1.0。
int display(...)
{
//...
glClear(GL_DEPTH_BUFFER_BIT);//清除深度缓冲
//...
glEnable(GL_DEPTH_TEST);//开启深度测试
glDepthFunc(GL_LEQUAL);//深度Func
}
//draw...
补充:”Z冲突”走样问题
当两个物体表面重叠时,这使得Z-Buffer算法难以确定应该渲染两个表面中的哪一个。此时,浮点误差可能会导致渲染出不自然的走样。
解决方案:
- 稍微移动一个物体避免重叠。
- 适当减少近裁剪面和远裁剪面之间的距离。(特别是近裁剪面不宜太近)
补充:性能优化
-
尽量减少内存分配:
不要在display()中声明变量或者实例化对象。 -
预先计算透视矩阵:
把透视矩阵的计算放在init()函数中,只计算一次。
(如果改变了窗口,可以在调用init()
之前使用glfwSetWindowSizeCallback(window, callback)
设置回调) -
背面剔除:
使用glEnable(GL_CULL_FACE)/glDisable(GL_CULL_FACE)
启用/禁用背面剔除。
使用glCullFace(GL_BACK/GL_FRONT/GL_GRONT_AND_BACK)
设置剔除背面/正面/全部剔除。
使用glFrontFace(GL_CCW/GL_CW)
显式设置3个顶点逆时针(默认)/顺时针排列时为正向。